Control Towerでアカウント作成時に管理アカウントからStep Functions(Lambda)を実行する
はじめに
AWS Control Towerを利用してマルチアカウントを管理する際、メンバーアカウントへの初期セットアップとしてCfCTやStackSetsを用いてリソースを展開するケースがあります。
これらはCloudFormationをアカウントが発行された際に自動で展開できるので非常に便利ではあるのですが、CloudFormationが未対応のリソースや既存設定の変更(パスワードポリシーの変更等)についてはLambdaのカスタムリソースを活用する必要があります。
しかし、この場合には各メンバーアカウントにLambdaを作成する必要があるため、ランタイムの管理などが少し煩雑になる懸念があります。
そこで今回は、各アカウントにLambdaを作成しないで済むように、管理アカウント側にセットアップ用のStep FunctionsとLambdaを作成し、新規作成されたアカウントへのアクセスを試してみました。
StepFunctionsからLambdaを実行しているのは、複数のLambda処理を入れたくなった時に順序性を持たせることを想定しているためです。
構成
今回作成したのは以下の構成です。
アカウント作成時にControl TowerからCreateManagedAccount
というイベントが発行されるので、それをEventBridgeで取得しています。EventBridgeのターゲットにStep Functionsを指定して、Lambda内でメンバーアカウントにあるAWSControlTowerExecution
へAssumeRoleします。今回は実行できることを確認するため、sts:get_caller_identity
を実行してみます。
AWSControlTowerExecution
このロールはControl Towerが個々のアカウントを管理できるように、登録時に作成されているロールのため個別に作成する必要はありません。Control Tower配下のアカウントには必ず作成されており、以下のように管理アカウントからAssumeRoleが許可されています。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<管理アカウントID>:root" }, "Action": "sts:AssumeRole" } ] }
このロールはAdministratorAccess権限を持っているため、管理アカウントからAssumeRoleすることで自由にAPIを実行できます。
より詳細に知りたい方は以下ドキュメントをご参照下さい。
AWS Control Tower がロールと連携してアカウントを作成および管理する方法 - AWS Control Tower
Lambdaロール(account-setup-lambda-role)
Lambdaにアタッチするロールは単純にAssumeRoleとログ出力を行う権限のみをつけます。
アタッチするポリシーは以下の通りです。
{ "Version": "2012-10-17", "Statement": [ { "Action": [ "sts:AssumeRole" ], "Resource": "*", "Effect": "Allow" }, { "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*", "Effect": "Allow" } ] }
Lambdaで実行するコード
実行するコードは、ほぼ同じことを監査アカウントから実施していたブログがあったのでお借りしました。(川原さんに感謝)
ランタイムは Python 3.8 で作成しました。
import boto3 import logging logger = logging.getLogger() logger.setLevel(logging.INFO) def _sts_client(target_account_id): logger.info('[START] _sts_client') sts_connection = boto3.client('sts') try: # Assume Role role_arn = "arn:aws:iam::%s:role/AWSControlTowerExecution" % target_account_id role_session_name = "CROSS_ACCOUNT_ACCESS_FROM_CTAUDIT" logger.info("- RoleArn=%s" % role_arn) logger.info("- RoleSessionName=%s" % role_session_name) target = sts_connection.assume_role( RoleArn=role_arn, RoleSessionName=role_session_name, ) except Exception as e: logger.error(e) exit() else: client = boto3.client( 'sts', aws_access_key_id=target['Credentials']['AccessKeyId'], aws_secret_access_key=target['Credentials']['SecretAccessKey'], aws_session_token=target['Credentials']['SessionToken'] ) logger.info('[END] _sts_client') return client def check(sts_client): logger.info('[START] check') try: resp = sts_client.get_caller_identity() logger.info("- Account=%s" % resp['Account']) logger.info("- Arn=%s" % resp['Arn']) except Exception as e: logger.error(e) exit() else: logger.info('[END] check') return {"status": "success"} def lambda_handler(event, context): logger.info('[START] lambda_handler') member_account_id=event["detail"]["serviceEventDetails"]["createManagedAccountStatus"]["account"]["accountId"] # Get Client sts_client = _sts_client(member_account_id) # Run remediation(check) logger.info('# running check') results = check(sts_client) # End logger.info('[END] lambda_handler') return results
メンバーアカウントのIDを取得する部分とAssumeRole先のロールを変更しています。
Lambdaへ送られてくるイベントCreateManagedAccount
の詳細については以下をご参照ください。
AWS Control Tower でのライフサイクルイベント - AWS Control Tower
Step Functions State Machine
ただLambdaを実行するだけのステートマシンを作成します。
定義情報は以下の通りです。
{ "Comment": "A description of my state machine", "StartAt": "Lambda Invoke", "States": { "Lambda Invoke": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "Payload.$": "$", "FunctionName": "test-account-setup-Lambda:$LATEST" }, "Retry": [ { "ErrorEquals": [ "Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException" ], "IntervalSeconds": 2, "MaxAttempts": 6, "BackoffRate": 2 } ], "End": true } } }
また今回の場合、ステートマシンにアタッチするIAM Role にはLambdaを呼び出す権限が必要です。
{ "Version": "2012-10-17", "Statement": [ { "Action": [ "lambda:InvokeFunction" ], "Resource": "<呼び出したい Lambda の ARN>:*", "Effect": "Allow" }, { "Action": [ "lambda:InvokeFunction" ], "Resource": "<呼び出したい Lambda の ARN>", "Effect": "Allow" } ] }
EventBridge Rule
最後にControl Towerからのライフサイクルイベントを取得するEventBridge Ruleを作成します。ここは以下のブログを大いに参考にさせて頂きました。(大前さんに感謝)
CreateManagedAccount
を取得できればいいので、以下のイベントパターンを定義します。
{ "source": ["aws.controltower"], "detail-type": ["AWS Service Event via CloudTrail"], "detail": { "eventName": ["CreateManagedAccount"] } }
コンソールからサービスをControl Towerで指定すると、CreateManagedAccount
のイベントパターンを自動でも生成できます。
ターゲットには先ほど作成したStep Functionsのステートマシンを指定する必要があります。
既存のロールを使用していますが、ステートマシンを実行する権限を付与したロールを選択しています。
{ "Version": "2012-10-17", "Statement": [ { "Action": [ "states:StartExecution" ], "Resource": [ "" ], "Effect": "Allow" } ] }
やってみる
それでは実際にアカウントを発行して動作するか確認してみます。Account Factory からアカウントを発行して登録されると、作成したStep Functions が実行されたことを確認できます。
実行されたLambdaのログを確認してみると、実行先のアカウントIDとAWSControlTowerExecution
に AssumeRole できていることが確認できました。
おわりに
管理アカウントのLambdaからメンバーアカウント上のAWSControlTowerExecution
に AssumeRole して動かしてみました。今回はアクセスできるかを確認するため、sts:get_caller_identity
のみ実行しただけですが、コードさえ書けばいいのでセットアップの幅は非常に広いです。
もしControl Tower環境でCloudFormationでは実現しにくいものセットアップ処理が必要になった際は、Step Functionsを自由に編集して活用頂ければ嬉しいです。
CloudFormationテンプレート
今回の構成を一発で作成できるテンプレートを置いておきます。実際にStep FunctionsとLambdaを作り込む場合は SAM や Serverless Framework を使うといい感じに管理できるかと思います。あくまでご参考程度にご利用ください。
AWSTemplateFormatVersion: "2010-09-09" Description: "Cross-account setup template" Parameters: # 作成リソースに付与する接頭語 Prefix: Description: "Prefix of each resource" Type: "String" Default: "test" Resources: # Step Functions StateMachine StateMachine: Type: "AWS::StepFunctions::StateMachine" Properties: StateMachineName: !Sub "${Prefix}-account-setup-statemachine" DefinitionString: !Sub | { "Comment": "A description of my state machine", "StartAt": "Lambda Invoke", "States": { "Lambda Invoke": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "OutputPath": "$.Payload", "Parameters": { "Payload.$": "$", "FunctionName": "${LambdaFunction}:$LATEST" }, "Retry": [ { "ErrorEquals": [ "Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException" ], "IntervalSeconds": 2, "MaxAttempts": 6, "BackoffRate": 2 } ], "End": true } } } RoleArn: !GetAtt StateMachineRole.Arn Tags: - Key: "Name" Value: !Sub "${Prefix}-account-setup-statemachine" # Lambda Function LambdaFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub "${Prefix}-account-setup-Lambda" Role: !GetAtt "LambdaExecutionRole.Arn" Runtime: "python3.8" Handler: index.lambda_handler Timeout: "180" Code: ZipFile: | import boto3 import logging logger = logging.getLogger() logger.setLevel(logging.INFO) def _sts_client(target_account_id): logger.info('[START] _sts_client') sts_connection = boto3.client('sts') try: # Assume Role role_arn = "arn:aws:iam::%s:role/AWSControlTowerExecution" % target_account_id role_session_name = "CROSS_ACCOUNT_ACCESS_FROM_CTAUDIT" logger.info("- RoleArn=%s" % role_arn) logger.info("- RoleSessionName=%s" % role_session_name) target = sts_connection.assume_role( RoleArn=role_arn, RoleSessionName=role_session_name, ) except Exception as e: logger.error(e) exit() else: client = boto3.client( 'sts', aws_access_key_id=target['Credentials']['AccessKeyId'], aws_secret_access_key=target['Credentials']['SecretAccessKey'], aws_session_token=target['Credentials']['SessionToken'] ) logger.info('[END] _sts_client') return client def check(sts_client): logger.info('[START] check') try: resp = sts_client.get_caller_identity() logger.info("- Account=%s" % resp['Account']) logger.info("- Arn=%s" % resp['Arn']) except Exception as e: logger.error(e) exit() else: logger.info('[END] check') return {"status": "success"} def lambda_handler(event, context): logger.info('[START] lambda_handler') member_account_id=event["detail"]["serviceEventDetails"]["createManagedAccountStatus"]["account"]["accountId"] # Get Client sts_client = _sts_client(member_account_id) # Run remediation(check) logger.info('# running check') results = check(sts_client) # End logger.info('[END] lambda_handler') return results ## IAM Role for Lambda LambdaExecutionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "${Prefix}-account-setup-lambda-role" AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/service-role/" Policies: - PolicyName: !Sub "${Prefix}-account-setup-lambda-policy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "sts:AssumeRole" Resource: "*" - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*" ## IAM Role for StateMachine StateMachineRole: Type: "AWS::IAM::Role" Properties: RoleName: !Sub "${Prefix}-account-setup-statemachine-role" Path: "/service-role/" AssumeRolePolicyDocument: Statement: - Effect: "Allow" Principal: Service: - "states.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: !Sub "${Prefix}-account-setup-statemachine-policy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "lambda:InvokeFunction" Resource: !Join - "" - - !GetAtt LambdaFunction.Arn - ":*" - Effect: Allow Action: - "lambda:InvokeFunction" Resource: !GetAtt "LambdaFunction.Arn" # EventRule for CreateManagedAccount EventRule: Type: "AWS::Events::Rule" Properties: Description: "Send CreateManagedAccount event to StateMachine" EventPattern: |- { "source": ["aws.controltower"], "detail-type": ["AWS Service Event via CloudTrail"], "detail": { "eventName": ["CreateManagedAccount"] } } Name: !Sub "${Prefix}-catch-CreateManagedAccount" State: "ENABLED" Targets: - Arn: !Ref StateMachine Id: !Sub "${Prefix}-target-account-setup-statemachine" RoleArn: !GetAtt EventBridgeRole.Arn ## IAM Role for EventBridge EventBridgeRole: Type: "AWS::IAM::Role" Properties: RoleName: !Sub "${Prefix}-catch-CreateManagedAccount-role" Path: "/service-role/" AssumeRolePolicyDocument: Statement: - Effect: "Allow" Principal: Service: - "events.amazonaws.com" Action: - "sts:AssumeRole" ManagedPolicyArns: - !Ref InvokeStepFunctionsPolicy ## IAM Policy to invoke Step Functions InvokeStepFunctionsPolicy: Type: "AWS::IAM::ManagedPolicy" Properties: ManagedPolicyName: !Sub "${Prefix}-invoke-step-functions-from-eventbridge-policy" Path: "/service-role/" PolicyDocument: Version: "2012-10-17" Statement: - Resource: - !Ref StateMachine Effect: "Allow" Action: - "states:StartExecution"
参考
- Control Towerカスタマイズソリューション(CfCT)を使ってガードレールとCloudFormationを自動展開してみた | DevelopersIO
- Organizationsのメンバーアカウントに、独自ベースラインのCloudFormationテンプレートをStackSetsで自動デプロイする | DevelopersIO
- 【AWS Control Tower】監査アカウントからメンバーアカウントへの Lambda アクセスを試す | DevelopersIO
- AWS Control Tower でアカウント作成する際の AWS SSO への権限セット割り当てを自動化してみた | DevelopersIO
- AWS Control Tower でのライフサイクルイベント - AWS Control Tower
- AWS Control Tower がロールと連携してアカウントを作成および管理する方法 - AWS Control Tower